Skip to content

Stabilize if let guards (feature(if_let_guard)) #141295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Kivooeo
Copy link
Contributor

@Kivooeo Kivooeo commented May 20, 2025

Summary

This proposes the stabilization of if let guards (tracking issue: #51114, RFC: rust-lang/rfcs#2294). This feature enhances Rust's pattern matching by allowing if let expressions to be used directly within match arm guards, enabling more expressive, concise, and readable conditional logic in match statements.

What is being stabilized

The ability to use if let expressions within match arm guards is being stabilized. This allows for conditional pattern matching directly within the guard clause of a match arm, combining pattern destructuring with boolean conditions.

Example:

enum Command {
    Run(String),
    Stop,
    Pause,
}

fn process_command(cmd: Command, state: &mut String) {
    match cmd {
        Command::Run(name) if let Some(first_char) = name.chars().next() && first_char.is_ascii_alphabetic() => {
            // Both `name` and `first_char` are available here
            println!("Running command: {} (starts with '{}')", name, first_char);
            state.push_str(&format!("Running {}", name));
        }
        Command::Run(name) => {
            println!("Cannot run command '{}'. Invalid name.", name);
        }
        Command::Stop if state.contains("running") => {
            println!("Stopping current process.");
            state.clear();
        }
        _ => {
            println!("Unhandled command or state.");
        }
    }
}

fn main() {
    let mut current_state = String::new();
    process_command(Command::Run("my_app".to_string()), &mut current_state);
    process_command(Command::Run("123_app".to_string()), &mut current_state);
    process_command(Command::Stop, &mut current_state); // Will not stop due to state
    process_command(Command::Run("another_app".to_string()), &mut current_state);
    process_command(Command::Stop, &mut current_state); // Will stop
}

Motivation

The primary motivation for if let guards is to reduce boilerplate and improve the clarity of conditional logic within match statements. Prior to this feature, complex conditional checks often required nested if let statements within the match arm's body, leading to increased indentation and reduced readability.

Consider the following scenario without if let guards:

match value {
    Some(x) => {
        if let Ok(y) = compute(x) {
            // Both `x` and `y` are available here
            println!("{}, {}", x, y);
        }
    }
    _ => {}
}

With if let guards, this becomes significantly more streamlined:

match value {
    Some(x) if let Ok(y) = compute(x) => {
        // Both `x` and `y` are available here
        println!("{}, {}", x, y);
    }
    _ => {}
}

Also I should make a remark here and say that drop order or other things are identical in this both contructions, so think about if let guards as just like about easirer way to express this

Implementation and Testing

The if let guard feature has undergone extensive implementation and testing to ensure its stability, correctness, and consistent behavior across all Rust editions. This process has involved collaborative efforts, notably with @est31, who has provided invaluable insights and thorough testing, confirming identical functionality across editions.

Tests

Error messages and diagnostics

warns.rs, parens.rs, macro-expanded.rs, guard-mutability-2.rs, ast-validate-guards.rs - shows that if let guards are parsed with strict syntax rules, disallowing parentheses around the let expression and rejecting macro expansions that produce statements instead of expressions. The compiler correctly diagnoses irrefutable patterns, unreachable patterns, mutability violations, and unsupported constructs inside guards, ensuring soundness and clear error messages. The parser fails on invalid syntax without complex recovery, preventing cascading errors and maintaining clarity

Scoping and shadowing

scope.rs - verifies that bindings created inside if let match guards are properly scoped and usable in the corresponding match arm. Covers both let on the left-hand side and right-hand side of &&

shadowing.rs - validates that name shadowing works correctly within if let guards and does not lead to resolution issues. Demonstrates deep shadowing via multiple lets and ensures type resolution matches expected shadowed bindings

scoping-consistency.rs - ensures that temporaries created within if let guards are correctly scoped to the guard expression and remain valid for the duration of the match arm they’re used in

Exhaustiveness

exhaustive.rs - validates that if let guards do not affect exhaustiveness checking in match expressions, test fails due missing match arm

Type System

type-inference.rs - confirms that type inference works correctly in match guards using if let

typeck.rs - verifies that type mismatches in if let guards are caught as expected — the same way they are in other contexts

Drop

drop-order.rs - ensures that temporaries created in match guards are dropped in the correct order

compare-drop-order.rs - comparing drop order between regular if let in match arm and if let guard between all editions to show that drop order across all editions the same

drop-score.rs - ensures that temporaries introduced in if let guards (including let chains) live for the duration of the arm

drop-order-comparisons.rs - compares evaluation and drop order between various kinds of let chains

Move

move-guard-if-let.rs, move-guard-if-let-chain.rs - tests verify that the borrow checker correctly understands how moves happen inside if let guards, especially with let chains. Specifically, a move of a variable inside a guard pattern only actually occurs if that guard pattern matches. This means the move is conditional, and the borrow checker must track this precisely to avoid false move errors or unsound behavior


Key aspects verified during testing include:

  • Variable Scope: Variables bound in the main pattern are accessible within the guard expression, and variables bound in the guard are accessible within the match arm body.
  • Chaining with &&: Multiple let bindings can be chained within a single guard using the && operator.
  • Refutable Patterns: The patterns within the if let guard can be refutable, allowing for concise conditional logic based on the success or failure of the pattern match.
  • MIR Generation: The Mid-level Intermediate Representation (MIR) generated for if let guards has been carefully reviewed to ensure it aligns with the expected runtime behavior, particularly concerning variable lifetimes and drop order.

Concers about all editions and drop order

Important

Unlike let chains in while and regular if the drop order in if let guard is predictable and stable across all editions and even if we writes let chains in if let guard it also works stable across all editions, this was tested by #140981 and compare-drop-order.rs

Any initial concerns regarding temporary drop order or variable scoping in older editions were resolved by backported compiler improvements, ensuring consistent and correct behavior across all editions

Analysis on interactions with guard patterns

  • @dianne, @max-niederman and @Nadrieril don’t see any problems with stabilizing if let guards first, drop order and scoping already work fine here and are similar to what guard patterns will need anyway
  • @dianne mentioned plans about submit a RFC to explore “if let guard patterns” in the future, but that shouldn’t block the current stabilization, as current if let guards don’t make anything harder for that
  • there’s also some open questions about more complex cases like Some(x if let Some(y) = y && x > 5) — but again, this can be addressed when guard patterns are fully designed and doesn’t seem like a blocker now

So based on this, it looks like we’re in a good position to move forward with if let guards without causing problems for future features.

Unresolved Issues

Note

This is my first stabilization PR, so if I missed any steps or there’s something I should adjust, please feel free to point it out — I’d be happy to improve it. I’ve followed the standard process as closely as I could, but I’m still learning the full stabilization workflow

Related

@rustbot
Copy link
Collaborator

rustbot commented May 20, 2025

r? @SparrowLii

rustbot has assigned @SparrowLii.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 20, 2025
@rustbot
Copy link
Collaborator

rustbot commented May 20, 2025

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

Some changes occurred in compiler/rustc_codegen_ssa

cc @WaffleLapkin

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 6fe74d9 to 5ee8970 Compare May 20, 2025 17:08
@rustbot
Copy link
Collaborator

rustbot commented May 20, 2025

rust-analyzer is developed in its own repository. If possible, consider making this change to rust-lang/rust-analyzer instead.

cc @rust-lang/rust-analyzer

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from eb0e4b4 to 0358002 Compare May 20, 2025 17:13
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 92a5204 to ab138ce Compare May 20, 2025 17:35
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 5ceca48 to a20c4f6 Compare May 20, 2025 17:57
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch 2 times, most recently from 1dd9974 to 5796073 Compare May 20, 2025 18:56
@traviscross traviscross added T-lang Relevant to the language team, which will review and decide on the PR/issue. needs-fcp This change is insta-stable, so needs a completed FCP to proceed. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 20, 2025
@traviscross
Copy link
Contributor

cc @est31 @ehuss

@traviscross
Copy link
Contributor

cc @Nadrieril

@SparrowLii
Copy link
Member

SparrowLii commented May 21, 2025

This needs a fcp so I'd like to roll this to someone more familiar with this feature
r? compiler

@rustbot rustbot added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label May 21, 2025
@rustbot rustbot assigned oli-obk and unassigned SparrowLii May 21, 2025
@traviscross traviscross added I-lang-nominated Nominated for discussion during a lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang labels May 21, 2025
@oli-obk
Copy link
Contributor

oli-obk commented May 21, 2025

r? @est31

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 831fca2 to 6270aba Compare May 21, 2025 17:47
@rustbot rustbot removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. has-merge-commits PR has merge commits, merge with caution. labels May 21, 2025
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 266e9d9 to 58b02c9 Compare May 21, 2025 17:56
@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 21, 2025

@scottmcm

Thanks for your comment! Just to clarify, the drop order appears to be consistent across all editions. Could you please clarify what you mean by the potential confusion or breaking change for users? Are you referring to specific behavioral differences or edge cases that might cause code to behave differently when moving from a normal if to a match arm if let guard?

It would be helpful to understand the exact scenarios you’re concerned about

@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from fdec32f to ba28c26 Compare May 21, 2025 18:29
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 0699298 to d6688ef Compare May 21, 2025 19:26
@traviscross
Copy link
Contributor

We reviewed this briefly in the lang triage call today. We'll all probably need to have a closer look at this, and we'll of course be particularly interested in confirming that the drop order here is what we expect.

@Nadrieril
Copy link
Member

Musing: it's not obvious to me that stabilizing this in all editions is the best answer, even if I agree it's technically legal.

How should I weigh the potential confusion of "wait, I moved it from a normal if to a match arm if and it broke?" here?

I'm missing context, what breakage are you talking about? @Kivooeo has confirmed that moving from a if let guard to a if let in the match arm (or vice-versa) does not change behavior regardless of edition.

@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 21, 2025

@traviscross I need your advice on this, would writing MIR drop order test for if-let guard will help team review it better and more precisely along side two existed (non MIR tests) that created for drop order: compare-drop-order, drop-order

The same way it was made for let chains back in the day here mir-match-guard-let-chains-drop-order

@Nadrieril
Copy link
Member

Nadrieril commented May 21, 2025

@Kivooeo the test you link is not a mir test, it's a normal (ui) test in the "mir" directory. A real MIR test is not necessary for this feature, since drop order can be observed with normal tests such as the ones you point to. It also seems to me that the existing tests are sufficient to demonstrate the drop order, unless there's a corner case I'm not thinking of.

@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 21, 2025

@Nadrieril, yes :) I know it and specify this in parenthesis

along side two existed (non MIR tests) that created for drop order: compare-drop-order, drop-order

Well, I thought that this is a MIR test because it was in mir folder, hm, interesting, is there a real MIR tests? I guess it should do something like comparing right MIR output to that one that created by programm or something

So this is what am I trying to understand, would MIR test helpful when we do have this ui tests or not really

@est31
Copy link
Member

est31 commented May 21, 2025

we'll of course be particularly interested in confirming that the drop order here is what we expect.

@traviscross as a good starting point, you can look at the tests added by #140981. The PR description gives an overview.

@traviscross traviscross removed the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label May 22, 2025
@traviscross
Copy link
Contributor

After we accepted rust-lang/rfcs#2294, we accepted:

Work is ongoing on the implementation of that. If you would, please include analysis and discussion in the stabilization report about the expected interaction between these features, and in particular, anything about this stabilization that might commit us to decisions about how guard patterns would have to work in combination with let guards or that might make the later stabilization of guard patterns more challenging.

cc @max-niederman @dianne

@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 203c6fe to d2cc457 Compare May 22, 2025 06:08
@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 22, 2025

@traviscross about the guard patterns feature and future interactions between if let guard, is there way i can check this locally at the moment? because if i get rfc right it allows us to use this syntax

match x {
    Some(x if x > 5) => (),
    None => (),
}

but when im trying to use #![feature(guard_patterns)] it's not let me do this and saying that this feature is incomplete, so it's just should be theoretical analysis of their possible future interactions, right?

i also should clarify if we looking into possibilities of this syntax like this

Some(x if let Some(y) = y && x > 5)

or only for something like this

Some(x if x > 5) if let Some(y) = y

because there was a message about this
#51114 (comment)

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 9e373c5 to 02361f2 Compare May 22, 2025 08:49
@traviscross
Copy link
Contributor

@Kivooeo; you'll probably just want to ask and talk with the people working on it, particularly @max-niederman, @dianne, and @Nadrieril.

@dianne
Copy link
Contributor

dianne commented May 22, 2025

Here's my initial read on the interactions:

  • The guard patterns RFC specifies that guard patterns should supersede match guards, but leaves if let guard patterns as a future possibility. This technically means the guard patterns RFC will be out-of-date, but I personally don't think it should block moving forward with if let guards. I'd like if let guard patterns, and consider future-compatibility with them important to the semantics/implementation of boolean guard patterns. I'm not familiar with stabilization logistics, though; my intent is to submit an updated RFC for if let guard patterns at some point (and/or see if I can experiment with if let guard patterns after the current RFC is implemented), but if there's a desire to stabilize boolean guard patterns before then, there might be an inconsistent feeling to allowing if let in match arms but not guard patterns. For what it's worth, I'm personally in favor of blocking stabilization of guard patterns on if let guard patterns, to make absolutely sure we don't accidentally stabilize ourselves into future-incompatibility.
  • As far as Some(x if x > 5) if let Some(y) = y => ... is concerned, semantically that's the same as Some(x) if x > 5 && let Some(y) = y => .... I don't think there's any problematic interactions here at a glance, at least. Judging by Add match guard let chain drop order and scoping tests #140981, it looks like let chains in if let guards have the correct drop order for everything to work out. It working on all editions is a great sign too, since the drop scope concerns for if let guard patterns are very similar to those of let chains in match guards.
  • if let guard patterns have their own subtleties, so implementing them won't be quite as simple as allowing let expressions in guard patterns. In that sense, it will make stabilization more challenging if it becomes a blocker.

@max-niederman
Copy link
Contributor

but when im trying to use #![feature(guard_patterns)] it's not let me do this and saying that this feature is incomplete, so it's just should be theoretical analysis of their possible future interactions, right?

Yes, that's correct. The feature is currently not fully implemented so it would just be analyzing future interactions.

i also should clarify if we looking into possibilities of this syntax like this

Some(x if let Some(y) = y && x > 5)

or only for something like this

Some(x if x > 5) if let Some(y) = y

because there was a message about this #51114 (comment)

As far as I'm aware, there hasn't been significant discussion of this yet, and we could go either way. The most conservative option would be to keep the special case for match arm guards and only allow if let there, but this would probably be confusing to users. The question is essentially whether there are any unintended consequences to allowing the former syntax.

@Nadrieril
Copy link
Member

Nadrieril commented May 22, 2025

Guard patterns are not implemented yet, it's normal that you can't experiment with it at the moment.

I do not foresee tricky interactions, curious if @dianne you can think of anything. My reasoning is as follows: pattern guards basically desugar to expanding or-patterns and moving the inside guards to be normal guards. So whatever the behavior of normal guards is what we'll get for guard patterns and that's probably ok.

Plus, honestly, I actually don't see any way in which if let guards could be implemented differently. If someone has an example in mind I'm all ears.

EDIT: looks like we all replied simultaneously

@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 22, 2025

updated PR description,

Also want to add my two cents on this, if i get everything right about how guard patterns should work

It desugar this

Some(x if x > 5)

to this

Some(x) if x > 5

so, technically, after if let guard get stabilized
code like this

Some(x if let Some(y) = y && x > 5)

will desugar to this

Some(x) if let Some(y) = y && x > 5

which is completly correct code with if let guard feature
so we should be fine with this?

even if user for some reason will write something like this

Some(if x > 5  && let Some(y) = y)

it also will desugars to correct code like

Some(x) if x > 5 && let Some(y) = y

so i currently see no problems with this features work together

correct me if im wrong
cc @max-niederman @dianne @Nadrieril

@dianne
Copy link
Contributor

dianne commented May 22, 2025

Yes, that's roughly how it works. Sorry if the following is off-topic/pedantic (hopefully it helps illustrate why these features work okay together!): technically there's not a desugaring pass in the implementation1, but the semantics are equivalent, so it's easiest to think of it as desugaring. Guard patterns are never represented as arm guards, but they do share a representation after a certain point in compilation. The representation you get from guard patterns in matches is the same as if you'd expanded out or-patterns and then written the guards as arm guards. e.g.,

Some(pat if guard1) if guard2 => /* ... */,

has the same runtime behavior as

Some(pat) if guard1 && guard2 => /* ... */,

and

[1 if guard1, (2 if guard2) | (3 if guard3), 4 if guard4] => /* ... */,

has the same runtime behavior as

[1, 2, 4] if guard1 && guard2 && guard4 => /* ... */,
[1, 3, 4] if guard1 && guard3 && guard4 => /* ... */,

You can also use guard patterns outside of matches, despite there not being arms to desugar to, e.g.

let (((min, max) if min <= max) | (max, min)) = (x, y);

which is where some of the subtleties crop up.

Footnotes

  1. Unfortunately not possible for anyone else to test yet, but I'll have a PR up soon hopefully!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-nominated Nominated for discussion during a lang team meeting. needs-fcp This change is insta-stable, so needs a completed FCP to proceed. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.